Skip to content

feat: enhance Jinja2 inline syntax highlighting#2715

Open
Neo2SHYAlien wants to merge 2 commits intoansible:mainfrom
Neo2SHYAlien:syntaxes-jinja2
Open

feat: enhance Jinja2 inline syntax highlighting#2715
Neo2SHYAlien wants to merge 2 commits intoansible:mainfrom
Neo2SHYAlien:syntaxes-jinja2

Conversation

@Neo2SHYAlien
Copy link
Copy Markdown

@Neo2SHYAlien Neo2SHYAlien commented Apr 4, 2026

  • Add statement ({% %}) and comment ({# #}) support with trim variants
  • Add missing keywords: break, continue, call/endcall, endfilter, with/endwith, extends, from, do, raw/endraw
  • Add ~ (concatenation), numeric literals, and comparison operator fixes
  • Support is not test syntax, variable assignment, and function calls
  • Separate logical operators (and, or, not, in) from control keywords
  • Add numeric dot-access (item.0) and whitespace tolerance after | and .
  • Fix >= operator (was =>), scope naming consistency (.jinja suffix)
  • Remove duplicate literal patterns from jinja-extras injection
  • Add until to conditional keywords, clean up scope names

Screenshots:
Before:
image

After:
image

- Add statement ({% %}) and comment ({# #}) support with trim variants
- Add missing keywords: break, continue, call/endcall, endfilter, with/endwith, extends, from, do, raw/endraw
- Add ~ (concatenation), numeric literals, and comparison operator fixes
- Support `is not` test syntax, variable assignment, and function calls
- Separate logical operators (and, or, not, in) from control keywords
- Add numeric dot-access (item.0) and whitespace tolerance after | and .
- Fix >= operator (was =>), scope naming consistency (.jinja suffix)
- Remove duplicate literal patterns from jinja-extras injection
- Add `until` to conditional keywords, clean up scope names
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Expanded and refined Jinja highlighting across Ansible grammars: added statement and comment injections, generalized delimiters with optional hyphens, renamed punctuation scopes, added until to conditional key matching, moved literal patterns to core grammar, and reclassified filters/tests/operators and numeric literals.

Changes

Cohort / File(s) Summary
Jinja Braces Injection
syntaxes/ansible/jinja-braces.tmLanguage.plist
Injection now recognizes {{ ... }}, {% ... %}, and {# ... #} with optional surrounding hyphens; begin/end captures use punctuation.section.embedded.begin/end.jinja; new jinja-statement and jinja-comment repository entries added.
Conditional Inline Embeds
syntaxes/ansible/jinja-conditionals.tmLanguage.plist
Added until to conditional-key matching (changed_when, failed_when, when, check_mode, until) across flow/block forms; changed embedded contentName to meta.embedded.inline.jinja for various scalar and string contexts.
Extras → Core Literal Move
syntaxes/ansible/jinja-extras.tmLanguage.plist
Removed #literals repository patterns (None/none, booleans, numeric formats); replaced with empty patterns and repository and added a comment that literals moved to core jinja.tmLanguage.json.
External Jinja Token Reclassification
syntaxes/external/jinja.tmLanguage.json
Re-scoped filters/tests to entity.name.function.*, separated assignment/identifier/function/operator rules, added logical operator rule (`and

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibbled at curly braces in the night,

{% and {{ now dancing right,
Comments curled in {# #} snug,
New scopes tucked in a grammar hug,
— a joyful rabbit hops with delight 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: enhance Jinja2 inline syntax highlighting' accurately reflects the primary changes: adding support for Jinja2 statements and comments, improving keyword coverage, and enhancing syntax highlighting across four grammar files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@syntaxes/ansible/jinja-braces.tmLanguage.plist`:
- Around line 6-8: Add a dedicated raw-block rule before the generic
'#jinja-statement' include so `{% raw %}` / `{%- raw -%}` (and matching `{%
endraw %}` / `{%- endraw -%}`) are treated as a raw region; implement a rule
(e.g. '#jinja-raw') that uses a begin pattern matching `{%[-\s]*raw[-\s]*%}`
allowing optional whitespace-control dashes and a corresponding end pattern
matching `{%[-\s]*endraw[-\s]*%}`, mark the content as raw (no jinja-expression
highlighting) and place this rule above the '#jinja-statement' include in the
list so inner `{{ ... }}` are not re-scanned; apply the same change to the other
occurrences mentioned (lines 29-46) to ensure consistent behavior.

In `@syntaxes/external/jinja.tmLanguage.json`:
- Line 379: The file ends with the final closing brace '}' but is missing a
trailing newline, causing the end-of-file-fixer CI error; add a single newline
character after the final '}' in jinja.tmLanguage.json so the file terminates
with a newline (restore the trailing newline at EOF).
- Around line 153-160: The current regex in the "match" property that assigns
entity.name.function.jinja uses a negative lookahead (?!and|or|not|in\b) which
also blocks identifiers starting with those sequences; update that negative
lookahead to require word boundaries around the keywords so only whole keywords
are excluded (i.e., change the lookahead to assert complete keyword boundaries)
while leaving the rest of the pattern ([a-zA-Z_][a-zA-Z0-9_]*\s*(?=\())))
unchanged so calls like order(...), notify(...), android(...) will be correctly
captured as functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f9ba6cd-04e6-4bb2-b946-b6a842260f65

📥 Commits

Reviewing files that changed from the base of the PR and between f771b37 and 0af1e08.

⛔ Files ignored due to path filters (6)
  • syntaxes/ansible/generated/jinja-braces.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-conditionals.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-double-quote-escape.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-extras.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-single-quote-escape.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/keywords.tmLanguage is excluded by !**/generated/**
📒 Files selected for processing (4)
  • syntaxes/ansible/jinja-braces.tmLanguage.plist
  • syntaxes/ansible/jinja-conditionals.tmLanguage.plist
  • syntaxes/ansible/jinja-extras.tmLanguage.plist
  • syntaxes/external/jinja.tmLanguage.json

Comment on lines +6 to +8
{ include = '#jinja-expression'; },
{ include = '#jinja-statement'; },
{ include = '#jinja-comment'; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does Jinja support raw blocks with whitespace-control delimiters like {%- raw -%}and{%- endraw -%}?

💡 Result:

Yes, Jinja supports raw blocks with whitespace-control delimiters like {%- raw -%} and {%- endraw -%}. The official Jinja documentation explicitly states this feature: "Note Minus sign at the end of {% raw -%} tag cleans all the spaces and newlines preceding the first character of your raw block." This applies to both the opening and closing tags, allowing full whitespace stripping (leading with - at start, trailing with - at end).

Citations:


Add a dedicated raw-block matcher before the generic statement rule.

{% raw %} / {%- raw -%} blocks are currently not matched as a dedicated rule and fall through to #jinja-statement, so the surrounding string continues to be scanned and any inner {{ ... }} still matches #jinja-expression. Jinja supports whitespace-control delimiters on raw blocks (as confirmed by official documentation), so the grammar should prevent content inside raw/endraw from being highlighted as expressions.

Suggested fix
 patterns = (
+        { include = '#jinja-raw-block'; },
         { include = '#jinja-expression'; },
         { include = '#jinja-statement'; },
         { include = '#jinja-comment'; }
 );
 ...
+        jinja-raw-block = {
+            begin = '\{%-?\s*raw\s*-?%\}';
+            end = '\{%-?\s*endraw\s*-?%\}';
+            name = 'comment.block.jinja.raw';
+        };
         jinja-statement = {

Also applies to: 29-46

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@syntaxes/ansible/jinja-braces.tmLanguage.plist` around lines 6 - 8, Add a
dedicated raw-block rule before the generic '#jinja-statement' include so `{%
raw %}` / `{%- raw -%}` (and matching `{% endraw %}` / `{%- endraw -%}`) are
treated as a raw region; implement a rule (e.g. '#jinja-raw') that uses a begin
pattern matching `{%[-\s]*raw[-\s]*%}` allowing optional whitespace-control
dashes and a corresponding end pattern matching `{%[-\s]*endraw[-\s]*%}`, mark
the content as raw (no jinja-expression highlighting) and place this rule above
the '#jinja-statement' include in the list so inner `{{ ... }}` are not
re-scanned; apply the same change to the other occurrences mentioned (lines
29-46) to ensure consistent behavior.

Comment on lines +153 to +160
{
"captures": {
"1": {
"name": "entity.name.function.jinja"
}
},
"match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import re

pattern = re.compile(r"\b(?!and|or|not|in\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")
samples = ["order(", "notify(", "android(", "input(", "foo(", "and(", "or(", "not(", "in("]

for sample in samples:
    match = pattern.search(sample)
    print(f"{sample:10} -> {match.group(1) if match else None}")
PY

Repository: ansible/vscode-ansible

Length of output: 236


🏁 Script executed:

python - <<'PY'
import re

# Current pattern (from the code)
current = re.compile(r"\b(?!and|or|not|in\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")

# Suggested fix pattern
suggested = re.compile(r"\b(?!(?:and|or|not|in)\b)([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()")

samples = ["order(", "notify(", "android(", "input(", "foo(", "and(", "or(", "not(", "in("]

print("Current pattern:")
for sample in samples:
    match = current.search(sample)
    print(f"  {sample:10} -> {match.group(1) if match else None}")

print("\nSuggested fix pattern:")
for sample in samples:
    match = suggested.search(sample)
    print(f"  {sample:10} -> {match.group(1) if match else None}")
PY

Repository: ansible/vscode-ansible

Length of output: 490


Tighten the negative lookahead on function names.

(?!and|or|not|in\b) also rejects identifiers that only start with and, or, or not, so calls like order(...), notify(...), or android(...) never get entity.name.function.jinja. The fix is to add a word boundary inside the lookahead to match only complete keywords.

Suggested fix
-                    "match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
+                    "match": "\\b(?!(?:and|or|not|in)\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"captures": {
"1": {
"name": "entity.name.function.jinja"
}
},
"match": "\\b(?!and|or|not|in\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
},
{
"captures": {
"1": {
"name": "entity.name.function.jinja"
}
},
"match": "\\b(?!(?:and|or|not|in)\\b)([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()"
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@syntaxes/external/jinja.tmLanguage.json` around lines 153 - 160, The current
regex in the "match" property that assigns entity.name.function.jinja uses a
negative lookahead (?!and|or|not|in\b) which also blocks identifiers starting
with those sequences; update that negative lookahead to require word boundaries
around the keywords so only whole keywords are excluded (i.e., change the
lookahead to assert complete keyword boundaries) while leaving the rest of the
pattern ([a-zA-Z_][a-zA-Z0-9_]*\s*(?=\()))) unchanged so calls like order(...),
notify(...), android(...) will be correctly captured as functions.

Comment thread syntaxes/external/jinja.tmLanguage.json Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
syntaxes/external/jinja.tmLanguage.json (1)

109-118: ⚠️ Potential issue | 🟡 Minor

Add dotted identifier support to the filter keyword and is test matchers.

Jinja2 allows dots in filter and test identifiers for hierarchical organization (e.g., ansible.utils.ipv4). The inline filter matcher at line 198 already supports this pattern, but the filter statement (line 118) and is test (line 132) matchers only accept simple identifiers, causing dotted names to be split and fall back to property scopes for the remainder.

Suggested fix
-                    "match": "\\s*\\b(filter)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\b"
+                    "match": "\\s*\\b(filter)\\s+([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\b"
-                    "match": "\\b(is)\\s+(not\\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\\b"
+                    "match": "\\b(is)\\s+(not\\s+)?([a-zA-Z_][a-zA-Z0-9_]*(?:\\.[a-zA-Z_][a-zA-Z0-9_]*)*)\\b"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@syntaxes/external/jinja.tmLanguage.json` around lines 109 - 118, Update the
regex for the "filter" keyword matcher and the "is" test matcher so identifiers
can include dotted components (e.g., ansible.utils.ipv4); replace the current
simple identifier pattern ([a-zA-Z_][a-zA-Z0-9_]*) with a dotted-identifier
pattern like [a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)* in the "match"
strings for the filter matcher (the block with "keyword.control.jinja" /
"entity.name.function.filter.jinja") and the corresponding "is" test matcher so
multi-part names are captured as a single token rather than split across scopes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@syntaxes/external/jinja.tmLanguage.json`:
- Around line 143-152: Add a Jinja "set"‑specific target matcher to cover
multi-target and block assignments: create a pattern used when parsing the "set"
tag that captures target names as "variable.parameter.jinja" when they are
followed by an equals sign, a comma, or the end of the statement (e.g. match
names with lookahead for "\\s*(?:=|,|\\s*%\\})"), and mark the "=" with
"keyword.operator.assignment.jinja" (replace or supplement the existing generic
assignment pattern "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)" and its captures)
so that both "{% set key, value = %}" and "{% set body %}...{% endset %}"
targets are highlighted.

---

Outside diff comments:
In `@syntaxes/external/jinja.tmLanguage.json`:
- Around line 109-118: Update the regex for the "filter" keyword matcher and the
"is" test matcher so identifiers can include dotted components (e.g.,
ansible.utils.ipv4); replace the current simple identifier pattern
([a-zA-Z_][a-zA-Z0-9_]*) with a dotted-identifier pattern like
[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)* in the "match" strings for
the filter matcher (the block with "keyword.control.jinja" /
"entity.name.function.filter.jinja") and the corresponding "is" test matcher so
multi-part names are captured as a single token rather than split across scopes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 382c5b23-26a9-4c37-a76b-d2027238a022

📥 Commits

Reviewing files that changed from the base of the PR and between 0af1e08 and da5f249.

⛔ Files ignored due to path filters (3)
  • syntaxes/ansible/generated/jinja-braces.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-conditionals.tmLanguage is excluded by !**/generated/**
  • syntaxes/ansible/generated/jinja-extras.tmLanguage is excluded by !**/generated/**
📒 Files selected for processing (1)
  • syntaxes/external/jinja.tmLanguage.json

Comment on lines +143 to +152
"captures": {
"1": {
"name": "variable.parameter.jinja"
},
"2": {
"name": "keyword.operator.assignment.jinja"
}
},
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)"
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check the file exists and read the lines in question
wc -l syntaxes/external/jinja.tmLanguage.json

Repository: ansible/vscode-ansible

Length of output: 109


🏁 Script executed:

# Read the context around lines 143-152 to see the current rule
sed -n '140,160p' syntaxes/external/jinja.tmLanguage.json

Repository: ansible/vscode-ansible

Length of output: 902


🏁 Script executed:

# Search for all "set" related rules in the file to see the full context
rg -n "set" syntaxes/external/jinja.tmLanguage.json -A 2 -B 2

Repository: ansible/vscode-ansible

Length of output: 439


🏁 Script executed:

# Read more context around the set rule - let's see a larger section
sed -n '130,160p' syntaxes/external/jinja.tmLanguage.json

Repository: ansible/vscode-ansible

Length of output: 1242


🏁 Script executed:

# Let's see the full structure of one of these rules to understand better
sed -n '130,180p' syntaxes/external/jinja.tmLanguage.json

Repository: ansible/vscode-ansible

Length of output: 2268


🌐 Web query:

Jinja2 set syntax multiple targets block assignments documentation

💡 Result:

Jinja2 supports multiple targets in the {% set %} tag for simultaneous variable assignments, similar to Python tuple unpacking. This works for regular variables and, since Jinja 3.2, also for namespace object attributes. Syntax examples from official documentation: Single variable: {% set navigation = [('index.html', 'Index'), ('about.html', 'About')] %} Multiple targets: {% set key, value = call_something %} For namespace objects (added in 2.10, multi-assignment in 3.2): {% set ns = namespace %} {% set ns.a, ns.b = 1, 2 %} Block assignments use {% set var %}...{% endset %} to capture block content into a single variable (added in 2.8, filters in 2.10). Multiple targets are not supported in block assignments; they assign to one variable only. Note: Variables set inside blocks/loops do not propagate outside due to scoping rules. Use namespaces for mutable state across scopes. Official documentation confirms this syntax is available in Jinja 3.1.x and 3.2.x (latest stable as of 2026).

Citations:


set targets are still only partially highlighted.

Jinja supports both multiple targets ({% set key, value = ... %}) and block assignments ({% set body %}...{% endset %}), but the current rules don't cover these cases. The generic keyword matcher explicitly excludes names followed by commas or equals, and the assignment matcher only marks names immediately before =. This leaves key unhighlighted in multi-target assignments and body unhighlighted in block assignments.

A set-specific target matcher that handles names followed by =, ,, or end-of-statement would correctly highlight assignment targets in all valid forms.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@syntaxes/external/jinja.tmLanguage.json` around lines 143 - 152, Add a Jinja
"set"‑specific target matcher to cover multi-target and block assignments:
create a pattern used when parsing the "set" tag that captures target names as
"variable.parameter.jinja" when they are followed by an equals sign, a comma, or
the end of the statement (e.g. match names with lookahead for
"\\s*(?:=|,|\\s*%\\})"), and mark the "=" with
"keyword.operator.assignment.jinja" (replace or supplement the existing generic
assignment pattern "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=)(?!=)" and its captures)
so that both "{% set key, value = %}" and "{% set body %}...{% endset %}"
targets are highlighted.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 4, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant